﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;

namespace System.Diagnostics.Metrics
{
    /// <summary>
    /// This EventSource is intended to let out-of-process tools (such as dotnet-counters) do
    /// ad-hoc monitoring for the new Instrument APIs. This source only supports one listener
    /// at a time. Each new listener will overwrite the configuration about which metrics
    /// are being collected and the time interval for the collection. In the future it would
    /// be nice to have support for multiple concurrent out-of-proc tools but EventSource's
    /// handling of filter arguments doesn't make that easy right now.
    ///
    /// Configuration - The EventSource accepts the following filter arguments:
    ///   - SessionId - An arbitrary opaque string that will be sent back to the listener in
    ///   many event payloads. If listener B reconfigures the EventSource while listener A
    ///   is still running it is possible that each of them will observe some of the events
    ///   that were generated using the other's requested configuration. Filtering on sessionId
    ///   allows each listener to ignore those events.
    ///   - RefreshInterval - The frequency in seconds for sending the metric time series data.
    ///   The format is anything parsable using double.TryParse(). Any
    ///   value less than AggregationManager.MinCollectionTimeSecs (currently 0.1 sec) is rounded
    ///   up to the minimum. If not specified the default interval is 1 second.
    ///   - Metrics - A semicolon separated list. Each item in the list is either the name of a
    ///   Meter or 'meter_name\instrument_name'. For example "Foo;System.Runtime\gc-gen0-size"
    ///   would include all instruments in the 'Foo' meter and the single 'gc-gen0-size' instrument
    ///   in the 'System.Runtime' meter.
    ///   - MaxTimeSeries - An integer that sets an upper bound on the number of time series
    ///   this event source will track. Because instruments can have unbounded sets of tags
    ///   even specifying a single metric could create unbounded load without this limit.
    ///   - MaxHistograms - An integer that sets an upper bound on the number of histograms
    ///   this event source will track. This allows setting a tighter bound on histograms
    ///   than time series in general given that histograms use considerably more memory.
    ///   - Base2ExponentialHistogram - Set the default aggregation configuration for histograms to base2 exponential.
    ///    If this is not specified, the default is to use the 'default' aggregation which is the exponential aggregation with the quantiles.
    ///    The value is a semicolon separated list of histogram aggregation specifications.
    ///       o scale - Maximum scale factor for Base2Exponential aggregation type. The default value is 20.
    ///       o maxBuckets - The maximum number of buckets for Base2Exponential aggregation type in each of the positive ranges,
    ///         not counting the special zero bucket. The default value is 160.
    ///       o reportDeltas - If true, the histogram will report deltas instead of whole accumulated values. The default value is false.
    /// </summary>
    [EventSource(Name = "System.Diagnostics.Metrics")]
    internal sealed partial class MetricsEventSource : EventSource
    {
        public static readonly MetricsEventSource Log = new();

        // Although this API isn't public, it is invoked via reflection from System.Private.CoreLib and needs the same back-compat
        // consideration as a public API. See EventSource.InitializeDefaultEventSources() in System.Private.CoreLib source for more
        // details. We have a unit test GetInstanceMethodIsReflectable that verifies this method isn't accidentally removed or renamed.
        public static MetricsEventSource GetInstance() { return Log; }

        private const string SharedSessionId = "SHARED";
        private const string ClientIdKey = "ClientId";
        private const string MaxHistogramsKey = "MaxHistograms";
        private const string MaxTimeSeriesKey = "MaxTimeSeries";
        private const string RefreshIntervalKey = "RefreshInterval";
        private const string DefaultValueDescription = "default";
        private const string SharedValueDescription = "shared value";

        public static class Keywords
        {
            /// <summary>
            /// Indicates diagnostics messages from MetricsEventSource should be included.
            /// </summary>
            public const EventKeywords Messages = (EventKeywords)0x1;
            /// <summary>
            /// Indicates that all the time series data points should be included
            /// </summary>
            public const EventKeywords TimeSeriesValues = (EventKeywords)0x2;
            /// <summary>
            /// Indicates that instrument published notifications should be included
            /// </summary>
            public const EventKeywords InstrumentPublishing = (EventKeywords)0x4;
        }

        private CommandHandler? _handler;

        private CommandHandler Handler
        {
            get
            {
                if (_handler == null)
                {
                    Interlocked.CompareExchange(ref _handler, new CommandHandler(this), null);
                }
                return _handler;
            }
        }

        /// <summary>
        /// Used to send ad-hoc diagnostics to humans.
        /// </summary>
        [Event(1, Keywords = Keywords.Messages)]
        public void Message(string? Message)
        {
            WriteEvent(1, Message);
        }

        [Event(2, Keywords = Keywords.TimeSeriesValues)]
#if !NET
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
                                      Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")]
#endif
        public void CollectionStart(string sessionId, DateTime intervalStartTime, DateTime intervalEndTime)
        {
            WriteEvent(2, sessionId, intervalStartTime, intervalEndTime);
        }

        [Event(3, Keywords = Keywords.TimeSeriesValues)]
#if !NET
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
                                      Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")]
#endif
        public void CollectionStop(string sessionId, DateTime intervalStartTime, DateTime intervalEndTime)
        {
            WriteEvent(3, sessionId, intervalStartTime, intervalEndTime);
        }

        [Event(4, Keywords = Keywords.TimeSeriesValues, Version = 2)]
#if !NET
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
                                      Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")]
#endif
        public void CounterRateValuePublished(string sessionId, string meterName, string? meterVersion, string instrumentName, string? unit, string tags, string rate, string value, int instrumentId)
        {
            WriteEvent(4, sessionId, meterName, meterVersion ?? "", instrumentName, unit ?? "", tags, rate, value, instrumentId);
        }

        [Event(5, Keywords = Keywords.TimeSeriesValues, Version = 2)]
#if !NET
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
                                      Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")]
#endif
        public void GaugeValuePublished(string sessionId, string meterName, string? meterVersion, string instrumentName, string? unit, string tags, string lastValue, int instrumentId)
        {
            WriteEvent(5, sessionId, meterName, meterVersion ?? "", instrumentName, unit ?? "", tags, lastValue, instrumentId);
        }

        [Event(6, Keywords = Keywords.TimeSeriesValues, Version = 2)]
#if !NET
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
                                      Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")]
#endif
        public void HistogramValuePublished(string sessionId, string meterName, string? meterVersion, string instrumentName, string? unit, string tags, string quantiles, int count, double sum, int instrumentId)
        {
            WriteEvent(6, sessionId, meterName, meterVersion ?? "", instrumentName, unit ?? "", tags, quantiles, count, sum, instrumentId);
        }

        // Sent when we begin to monitor the value of a instrument, either because new session filter arguments changed subscriptions
        // or because an instrument matching the pre-existing filter has just been created. This event precedes all *MetricPublished events
        // for the same named instrument.
        [Event(7, Keywords = Keywords.TimeSeriesValues, Version = 3)]
#if !NET
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
                                      Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")]
#endif
        public void BeginInstrumentReporting(
                        string sessionId,
                        string meterName,
                        string? meterVersion,
                        string instrumentName,
                        string instrumentType,
                        string? unit,
                        string? description,
                        string instrumentTags,
                        string meterTags,
                        string meterScopeHash,
                        int instrumentId,
                        string? meterTelemetrySchemaUrl)
        {
            WriteEvent(7, sessionId, meterName, meterVersion ?? "", instrumentName, instrumentType, unit ?? "", description ?? "",
                    instrumentTags, meterTags, meterScopeHash, instrumentId, meterTelemetrySchemaUrl ?? "");
        }

        // Sent when we stop monitoring the value of a instrument, either because new session filter arguments changed subscriptions
        // or because the Meter has been disposed.
        [Event(8, Keywords = Keywords.TimeSeriesValues, Version = 3)]
#if !NET
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
                                      Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")]
#endif
        public void EndInstrumentReporting(
                        string sessionId,
                        string meterName,
                        string? meterVersion,
                        string instrumentName,
                        string instrumentType,
                        string? unit,
                        string? description,
                        string instrumentTags,
                        string meterTags,
                        string meterScopeHash,
                        int instrumentId,
                        string? meterTelemetrySchemaUrl)
        {
            WriteEvent(8, sessionId, meterName, meterVersion ?? "", instrumentName, instrumentType, unit ?? "", description ?? "",
                    instrumentTags, meterTags, meterScopeHash, instrumentId, meterTelemetrySchemaUrl ?? "");
        }

        [Event(9, Keywords = Keywords.TimeSeriesValues | Keywords.Messages | Keywords.InstrumentPublishing)]
        public void Error(string sessionId, string errorMessage)
        {
            WriteEvent(9, sessionId, errorMessage);
        }

        [Event(10, Keywords = Keywords.TimeSeriesValues | Keywords.InstrumentPublishing)]
        public void InitialInstrumentEnumerationComplete(string sessionId)
        {
            WriteEvent(10, sessionId);
        }

        [Event(11, Keywords = Keywords.InstrumentPublishing, Version = 3)]
#if !NET
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
                                      Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")]
#endif
        public void InstrumentPublished(
                        string sessionId,
                        string meterName,
                        string? meterVersion,
                        string instrumentName,
                        string instrumentType,
                        string? unit,
                        string? description,
                        string instrumentTags,
                        string meterTags,
                        string meterScopeHash,
                        int instrumentId,
                        string? meterTelemetrySchemaUrl)
        {
            WriteEvent(11, sessionId, meterName, meterVersion ?? "", instrumentName, instrumentType, unit ?? "", description ?? "",
                    instrumentTags, meterTags, meterScopeHash, instrumentId, meterTelemetrySchemaUrl ?? "");
        }

        [Event(12, Keywords = Keywords.TimeSeriesValues)]
        public void TimeSeriesLimitReached(string sessionId)
        {
            WriteEvent(12, sessionId);
        }

        [Event(13, Keywords = Keywords.TimeSeriesValues)]
        public void HistogramLimitReached(string sessionId)
        {
            WriteEvent(13, sessionId);
        }

        [Event(14, Keywords = Keywords.TimeSeriesValues)]
        public void ObservableInstrumentCallbackError(string sessionId, string errorMessage)
        {
            WriteEvent(14, sessionId, errorMessage);
        }

        [Event(15, Keywords = Keywords.TimeSeriesValues | Keywords.Messages | Keywords.InstrumentPublishing)]
        public void MultipleSessionsNotSupportedError(string runningSessionId)
        {
            WriteEvent(15, runningSessionId);
        }

        [Event(16, Keywords = Keywords.TimeSeriesValues, Version = 2)]
#if !NET
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
                                      Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")]
#endif
        public void UpDownCounterRateValuePublished(string sessionId, string meterName, string? meterVersion, string instrumentName, string? unit, string tags, string rate, string value, int instrumentId)
        {
            WriteEvent(16, sessionId, meterName, meterVersion ?? "", instrumentName, unit ?? "", tags, rate, value, instrumentId);
        }

        [Event(17, Keywords = Keywords.TimeSeriesValues)]
#if !NET
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
                            Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")]
#endif
        public void MultipleSessionsConfiguredIncorrectlyError(string clientId, string expectedMaxHistograms, string actualMaxHistograms, string expectedMaxTimeSeries, string actualMaxTimeSeries, string expectedRefreshInterval, string actualRefreshInterval)
        {
            WriteEvent(17, clientId, expectedMaxHistograms, actualMaxHistograms, expectedMaxTimeSeries, actualMaxTimeSeries, expectedRefreshInterval, actualRefreshInterval);
        }

        /// <summary>
        /// Used to send version information.
        /// </summary>
        [Event(18, Keywords = Keywords.Messages)]
        public void Version(int Major, int Minor, int Patch)
        {
            WriteEvent(18, Major, Minor, Patch);
        }

        /// <summary>
        /// Used to send the value of a base 2 exponential histogram.
        /// </summary>
        [Event(19, Keywords = Keywords.TimeSeriesValues, Version = 1)]
#if !NET
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
                                      Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")]
#endif
        public void Base2ExponentialHistogramValuePublished(string sessionId, string meterName, string? meterVersion, string instrumentName, int instrumentId, string? unit, string tags, int scale, double sum,
                                                            long count, long zeroCount, double minimum, double maximum, string buckets)
        {
            WriteEvent(19, sessionId, meterName, meterVersion ?? "", instrumentName, instrumentId, unit ?? "", tags, scale, sum, count, zeroCount, minimum, maximum, buckets);
        }

        /// <summary>
        /// Called when the EventSource gets a command from a EventListener or ETW.
        /// </summary>
        [NonEvent]
        protected override void OnEventCommand(EventCommandEventArgs command)
        {
            if (command.Command == EventCommand.Enable)
            {
                Version(
                    ThisAssembly.AssemblyFileVersion.Major,
                    ThisAssembly.AssemblyFileVersion.Minor,
                    ThisAssembly.AssemblyFileVersion.Build);
            }

            lock (this)
            {
                Handler.OnEventCommand(command);
            }
        }

        // EventSource assumes that every method defined on it represents an event.
        // Methods that are declared explicitly can use the [NonEvent] attribute to opt-out but
        // lambdas can't. Putting all the command handling logic in this nested class
        // is a simpler way to opt everything out in bulk.
        private sealed class CommandHandler
        {
            private AggregationManager? _aggregationManager;
            private string _sessionId = "";
            private HashSet<string> _sharedSessionClientIds = new HashSet<string>();
            private int _sharedSessionRefCount;
            private bool _disabledRefCount;

            public CommandHandler(MetricsEventSource parent)
            {
                Parent = parent;
            }

            public MetricsEventSource Parent { get; }

            public bool IsSharedSession(string commandSessionId)
            {
                // commandSessionId may be null if it's the disable command
                return _sessionId.Equals(SharedSessionId) && (string.IsNullOrEmpty(commandSessionId) || commandSessionId.Equals(SharedSessionId));
            }

            public void OnEventCommand(EventCommandEventArgs command)
            {
                try
                {
#if OS_ISWASI_SUPPORT
                    if (OperatingSystem.IsWasi())
                    {
                        // AggregationManager uses a dedicated thread to avoid losing data for apps experiencing threadpool starvation
                        // and wasi doesn't support Thread.Start()
                        //
                        // This limitation shouldn't really matter because wasi also doesn't support out-of-proc EventSource communication
                        // which is the intended scenario for this EventSource. If it matters in the future AggregationManager can be
                        // modified to have some other fallback path that works for wasi.
                        Parent.Error("", "System.Diagnostics.Metrics EventSource not supported on wasi");
                        return;
                    }
#endif

                    string commandSessionId = GetSessionId(command);

                    if ((command.Command == EventCommand.Update
                        || command.Command == EventCommand.Disable
                        || command.Command == EventCommand.Enable)
                        && _aggregationManager != null)
                    {
                        if (command.Command == EventCommand.Update
                            || command.Command == EventCommand.Enable)
                        {
                            IncrementRefCount(commandSessionId, command);
                        }

                        if (IsSharedSession(commandSessionId))
                        {
                            if (ShouldDisable(command.Command))
                            {
                                Parent.Message($"Previous session with id {_sessionId} is stopped");
                                _aggregationManager.Dispose();
                                _aggregationManager = null;
                                _sessionId = string.Empty;
                                _sharedSessionClientIds.Clear();
                                return;
                            }

                            bool validShared = true;

                            double refreshInterval;
                            lock (_aggregationManager)
                            {
                                validShared = SetSharedRefreshIntervalSecs(command.Arguments!, _aggregationManager.CollectionPeriod.TotalSeconds, out refreshInterval) ? validShared : false;
                            }

                            validShared = SetSharedMaxHistograms(command.Arguments!, _aggregationManager.MaxHistograms, out int maxHistograms) ? validShared : false;
                            validShared = SetSharedMaxTimeSeries(command.Arguments!, _aggregationManager.MaxTimeSeries, out int maxTimeSeries) ? validShared : false;

                            if (command.Command != EventCommand.Disable)
                            {
                                if (validShared)
                                {
                                    if (ParseMetrics(command.Arguments!, out string? metricsSpecs))
                                    {
                                        ParseSpecs(metricsSpecs);
                                        _aggregationManager.Update();
                                    }

                                    if (ParseBase2ExponentialHistogramSpecs(command.Arguments!, out string? base2ExponentialHistogramSpec))
                                    {
                                        ParseBase2ExponentialHistogram(base2ExponentialHistogramSpec);
                                    }

                                    return;
                                }
                                else
                                {
                                    // If the clientId protocol is not followed, we can't tell which session is configured incorrectly
                                    if (command.Arguments!.TryGetValue(ClientIdKey, out string? clientId))
                                    {
                                        lock (_aggregationManager)
                                        {
                                            // Use ClientId to identify the session that is not configured correctly (since the sessionId is just SHARED)
                                            Parent.MultipleSessionsConfiguredIncorrectlyError(clientId!, _aggregationManager.MaxHistograms.ToString(), maxHistograms.ToString(), _aggregationManager.MaxTimeSeries.ToString(), maxTimeSeries.ToString(), _aggregationManager.CollectionPeriod.TotalSeconds.ToString(), refreshInterval.ToString());
                                        }
                                    }

                                    return;
                                }
                            }
                        }
                        else
                        {
                            if (command.Command == EventCommand.Enable || command.Command == EventCommand.Update)
                            {
                                // trying to add more sessions is not supported for unshared sessions
                                // EventSource doesn't provide an API that allows us to enumerate the listeners'
                                // filter arguments independently or to easily track them ourselves. For example
                                // removing a listener still shows up as EventCommand.Enable as long as at least
                                // one other listener is active. In the future we might be able to figure out how
                                // to infer the changes from the info we do have or add a better API but for now
                                // I am taking the simple route  and not supporting it.
                                Parent.MultipleSessionsNotSupportedError(_sessionId);
                                return;
                            }
                            else if (ShouldDisable(command.Command))
                            {
                                Parent.Message($"Previous session with id {_sessionId} is stopped");
                                _aggregationManager.Dispose();
                                _aggregationManager = null;
                                _sessionId = string.Empty;
                                _sharedSessionClientIds.Clear();
                                return;
                            }
                        }
                    }

                    if ((command.Command == EventCommand.Update || command.Command == EventCommand.Enable) && command.Arguments != null)
                    {
                        IncrementRefCount(commandSessionId, command);

                        _sessionId = commandSessionId;

                        double defaultIntervalSecs = 1;
                        Debug.Assert(AggregationManager.MinCollectionTimeSecs <= defaultIntervalSecs);
                        SetRefreshIntervalSecs(command.Arguments!, AggregationManager.MinCollectionTimeSecs, defaultIntervalSecs, out double refreshIntervalSecs);

                        const int defaultMaxTimeSeries = 1000;
                        SetUniqueMaxTimeSeries(command.Arguments!, defaultMaxTimeSeries, out int maxTimeSeries);

                        const int defaultMaxHistograms = 20;
                        SetUniqueMaxHistograms(command.Arguments!, defaultMaxHistograms, out int maxHistograms);

                        string sessionId = _sessionId;
                        _aggregationManager = new AggregationManager(
                            maxTimeSeries: maxTimeSeries,
                            maxHistograms: maxHistograms,
                            collectMeasurement: (i, s, state) => TransmitMetricValue(i, s, sessionId, state),
                            beginCollection: (startIntervalTime, endIntervalTime) => Parent.CollectionStart(sessionId, startIntervalTime, endIntervalTime),
                            endCollection: (startIntervalTime, endIntervalTime) => Parent.CollectionStop(sessionId, startIntervalTime, endIntervalTime),
                            beginInstrumentMeasurements: (i, state) => Parent.BeginInstrumentReporting(sessionId, i.Meter.Name, i.Meter.Version, i.Name, i.GetType().Name, i.Unit, i.Description,
                                    Helpers.FormatTags(i.Tags), Helpers.FormatTags(i.Meter.Tags), Helpers.FormatObjectHash(i.Meter.Scope), state.ID, i.Meter.TelemetrySchemaUrl),
                            endInstrumentMeasurements: (i, state) => Parent.EndInstrumentReporting(sessionId, i.Meter.Name, i.Meter.Version, i.Name, i.GetType().Name, i.Unit, i.Description,
                                    Helpers.FormatTags(i.Tags), Helpers.FormatTags(i.Meter.Tags), Helpers.FormatObjectHash(i.Meter.Scope), state.ID, i.Meter.TelemetrySchemaUrl),
                            instrumentPublished: (i, state) => Parent.InstrumentPublished(sessionId, i.Meter.Name, i.Meter.Version, i.Name, i.GetType().Name, i.Unit, i.Description,
                                    Helpers.FormatTags(i.Tags), Helpers.FormatTags(i.Meter.Tags), Helpers.FormatObjectHash(i.Meter.Scope), state is null ? 0 : state.ID, i.Meter.TelemetrySchemaUrl),
                            initialInstrumentEnumerationComplete: () => Parent.InitialInstrumentEnumerationComplete(sessionId),
                            collectionError: e => Parent.Error(sessionId, e.ToString()),
                            timeSeriesLimitReached: () => Parent.TimeSeriesLimitReached(sessionId),
                            histogramLimitReached: () => Parent.HistogramLimitReached(sessionId),
                            observableInstrumentCallbackError: e => Parent.ObservableInstrumentCallbackError(sessionId, e.ToString()));

                        _aggregationManager.SetCollectionPeriod(TimeSpan.FromSeconds(refreshIntervalSecs));

                        if (ParseMetrics(command.Arguments!, out string? metricsSpecs))
                        {
                            ParseSpecs(metricsSpecs);
                        }

                        if (ParseBase2ExponentialHistogramSpecs(command.Arguments!, out string? base2ExponentialHistogramSpec))
                        {
                            ParseBase2ExponentialHistogram(base2ExponentialHistogramSpec);
                        }

                        _aggregationManager.Start();
                    }
                }
                catch (Exception e) when (LogError(e))
                {
                    // this will never run
                }
            }

            private bool ShouldDisable(EventCommand command)
            {
                return command == EventCommand.Disable
                    && ((!_disabledRefCount && Interlocked.Decrement(ref _sharedSessionRefCount) == 0)
                    || !Parent.IsEnabled());
            }

            private bool ParseMetrics(IDictionary<string, string> arguments, out string? metricsSpecs)
            {
                if (arguments.TryGetValue("Metrics", out metricsSpecs))
                {
                    Parent.Message($"Metrics argument received: {metricsSpecs}");
                    return true;
                }

                Parent.Message("No Metrics argument received");
                return false;
            }

            private bool ParseBase2ExponentialHistogramSpecs(IDictionary<string, string> arguments, out string? base2ExponentialHistogramSpec)
            {
                if (arguments.TryGetValue("Base2ExponentialHistogram", out base2ExponentialHistogramSpec))
                {
                    Parent.Message($"Histogram Aggregation argument received: {base2ExponentialHistogramSpec}");
                    return true;
                }

                Parent.Message("No Histogram Aggregation argument received");
                return false;
            }

            private void InvalidateRefCounting()
            {
                _disabledRefCount = true;
                Parent.Message($"{ClientIdKey} not provided; session will remain active indefinitely.");
            }

            private void IncrementRefCount(string clientId, EventCommandEventArgs command)
            {
                // When creating a SHARED session (i.e. sessionId == SharedSessionId), a randomly-generated clientId
                // should be provided as part of the command arguments. If not, we can't tell which session is
                // configured incorrectly, and ref-counting will be disabled since there is no way to keep track of
                // multiple Enables coming from the same client. This will cause the session to remain active indefinitely.
                if (clientId.Equals(SharedSessionId))
                {
                    if (command.Arguments!.TryGetValue(ClientIdKey, out string? clientIdArg) && !string.IsNullOrEmpty(clientIdArg))
                    {
                        clientId = clientIdArg!;
                    }
                    else
                    {
                        // If ClientId contract is followed, this should never happen.
                        InvalidateRefCounting();
                    }
                }

                if (_sharedSessionClientIds.Add(clientId))
                {
                    Interlocked.Increment(ref _sharedSessionRefCount);
                }
            }

            private bool SetSharedMaxTimeSeries(IDictionary<string, string> arguments, int sharedValue, out int maxTimeSeries)
            {
                return SetMaxValue(arguments, MaxTimeSeriesKey, SharedValueDescription, sharedValue, out maxTimeSeries);
            }

            private void SetUniqueMaxTimeSeries(IDictionary<string, string> arguments, int defaultValue, out int maxTimeSeries)
            {
                _ = SetMaxValue(arguments, MaxTimeSeriesKey, DefaultValueDescription, defaultValue, out maxTimeSeries);
            }

            private bool SetSharedMaxHistograms(IDictionary<string, string> arguments, int sharedValue, out int maxHistograms)
            {
                return SetMaxValue(arguments, MaxHistogramsKey, SharedValueDescription, sharedValue, out maxHistograms);
            }

            private void SetUniqueMaxHistograms(IDictionary<string, string> arguments, int defaultValue, out int maxHistograms)
            {
                _ = SetMaxValue(arguments, MaxHistogramsKey, DefaultValueDescription, defaultValue, out maxHistograms);
            }

            private bool SetMaxValue(IDictionary<string, string> arguments, string argumentsKey, string valueDescriptor, int defaultValue, out int maxValue)
            {
                if (arguments.TryGetValue(argumentsKey, out string? maxString))
                {
                    Parent.Message($"{argumentsKey} argument received: {maxString}");
                    if (!int.TryParse(maxString, out maxValue))
                    {
                        Parent.Message($"Failed to parse {argumentsKey}. Using {valueDescriptor} {defaultValue}");
                        maxValue = defaultValue;
                    }
                    else if (maxValue != defaultValue)
                    {
                        // This is only relevant for shared sessions, where the "default" (provided) value is what is being
                        // used by the existing session.
                        return false;
                    }
                }
                else
                {
                    Parent.Message($"No {argumentsKey} argument received. Using {valueDescriptor} {defaultValue}");
                    maxValue = defaultValue;
                }

                return true;
            }

            private void SetRefreshIntervalSecs(IDictionary<string, string> arguments, double minValue, double defaultValue, out double refreshIntervalSeconds)
            {
                if (GetRefreshIntervalSecs(arguments, DefaultValueDescription, defaultValue, out refreshIntervalSeconds)
                    && refreshIntervalSeconds < minValue)
                {
                    Parent.Message($"{RefreshIntervalKey} too small. Using minimum interval {minValue} seconds.");
                    refreshIntervalSeconds = minValue;
                }
            }

            private bool SetSharedRefreshIntervalSecs(IDictionary<string, string> arguments, double sharedValue, out double refreshIntervalSeconds)
            {
                if (GetRefreshIntervalSecs(arguments, SharedValueDescription, sharedValue, out refreshIntervalSeconds)
                    && refreshIntervalSeconds != sharedValue)
                {
                    return false;
                }

                return true;
            }

            private bool GetRefreshIntervalSecs(IDictionary<string, string> arguments, string valueDescriptor, double defaultValue, out double refreshIntervalSeconds)
            {
                if (arguments!.TryGetValue(RefreshIntervalKey, out string? refreshInterval))
                {
                    Parent.Message($"{RefreshIntervalKey} argument received: {refreshInterval}");
                    if (!double.TryParse(refreshInterval, out refreshIntervalSeconds))
                    {
                        Parent.Message($"Failed to parse {RefreshIntervalKey}. Using {valueDescriptor} {defaultValue}s.");
                        refreshIntervalSeconds = defaultValue;
                        return false;
                    }
                }
                else
                {
                    Parent.Message($"No {RefreshIntervalKey} argument received. Using {valueDescriptor} {defaultValue}s.");
                    refreshIntervalSeconds = defaultValue;
                    return false;
                }

                return true;
            }

            private string GetSessionId(EventCommandEventArgs command)
            {
                if (command.Arguments!.TryGetValue("SessionId", out string? id))
                {
                    Parent.Message($"SessionId argument received: {id!}");
                    return id!;
                }

                string sessionId = string.Empty;

                if (command.Command != EventCommand.Disable)
                {
                    sessionId = Guid.NewGuid().ToString();
                    Parent.Message($"New session started. SessionId auto-generated: {sessionId}");
                }

                return sessionId;
            }

            private bool LogError(Exception e)
            {
                Parent.Error(_sessionId, e.ToString());
                // this code runs as an exception filter
                // returning false ensures the catch handler isn't run
                return false;
            }

            private static readonly char[] s_instrumentSeparators = new char[] { '\r', '\n', ',', ';' };

            private readonly char[] Base2ExponentialHistogramSpecSeparators = [';'];
            private const char HistogramPartSeparator = '=';

            private void ParseSpecs(string? metricsSpecs)
            {
                if (metricsSpecs == null)
                {
                    return;
                }

                string[] specStrings = metricsSpecs.Split(s_instrumentSeparators, StringSplitOptions.RemoveEmptyEntries);
                foreach (string specString in specStrings)
                {
                    MetricSpec spec = MetricSpec.Parse(specString);
                    Parent.Message($"Parsed metric: {spec}");
                    if (spec.InstrumentName != null)
                    {
                        _aggregationManager!.Include(spec.MeterName, spec.InstrumentName);
                    }
                    else if (spec.MeterName.Length > 0
                        && spec.MeterName[spec.MeterName.Length - 1] == '*')
                    {
                        if (spec.MeterName.Length == 1)
                        {
                            _aggregationManager!.IncludeAll();
                        }
                        else
                        {
                            _aggregationManager!.IncludePrefix(
                                spec.MeterName.Substring(0, spec.MeterName.Length - 1));
                        }
                    }
                    else
                    {
                        _aggregationManager!.Include(spec.MeterName);
                    }
                }
            }

            private void ParseBase2ExponentialHistogram(string? base2ExponentialHistogramSpec)
            {
                if (base2ExponentialHistogramSpec == null)
                {
                    return;
                }

                string[] specStrings = base2ExponentialHistogramSpec.Split(Base2ExponentialHistogramSpecSeparators, StringSplitOptions.RemoveEmptyEntries);

                if (specStrings.Length == 0)
                {
                    Parent.Message("No histogram aggregation spec is provided");
                    return;
                }

                // Default values for Base 2 exponential histogram
                int scale = 20;
                int maxBuckets = 160;
                bool reportDeltas = false;

                foreach (string specString in specStrings)
                {
                    int index = specString.IndexOf(HistogramPartSeparator);
                    if (index < 0)
                    {
                        Parent.Message($"Invalid histogram aggregation spec: {specString}");
                        continue;
                    }

                    ReadOnlySpan<char> spec = specString.AsSpan(0, index).Trim();
                    ReadOnlySpan<char> value = specString.AsSpan(index + 1).Trim();

                    if (spec.Equals("scale", StringComparison.OrdinalIgnoreCase))
                    {
#if NET
                        if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int s) || s < -11 || s > 20)
#else
                        if (!int.TryParse(value.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int s) || s < -11 || s > 20)
#endif // NET
                        {
                            Parent.Message($"Invalid scale value: {specString}");
                            continue;
                        }
                        else
                        {
                            scale = s;
                        }
                    }
                    else if (spec.Equals("maxBuckets", StringComparison.OrdinalIgnoreCase))
                    {
#if NET
                        if (!int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out int m) || m < 2)
#else
                        if (!int.TryParse(value.ToString(), NumberStyles.None, CultureInfo.InvariantCulture, out int m) || m < 2)
#endif // NET
                        {
                            Parent.Message($"Invalid maxBuckets value: {specString}");
                            continue;
                        }
                        else
                        {
                            maxBuckets = m;
                        }
                    }
                    else if (spec.Equals("reportDeltas", StringComparison.OrdinalIgnoreCase))
                    {
                        if (value.Equals("true", StringComparison.OrdinalIgnoreCase))
                        {
                            reportDeltas = true;
                        }
                        else if (value.Equals("false", StringComparison.OrdinalIgnoreCase))
                        {
                            reportDeltas = false;
                        }
                        else
                        {
                            Parent.Message($"Invalid reportDeltas value: {specString}");
                            continue;
                        }
                    }
                }

                _aggregationManager!.SetHistogramAggregation(() => new Base2ExponentialHistogramAggregator(maxBuckets, scale, reportDeltas));
            }

            private static void TransmitMetricValue(Instrument instrument, LabeledAggregationStatistics stats, string sessionId, InstrumentState? instrumentState)
            {
                int instrumentId = instrumentState?.ID ?? 0;
                if (stats.AggregationStatistics is CounterStatistics rateStats)
                {
                    if (rateStats.IsMonotonic)
                    {
                        Log.CounterRateValuePublished(sessionId, instrument.Meter.Name, instrument.Meter.Version, instrument.Name, instrument.Unit, Helpers.FormatTags(stats.Labels),
                            rateStats.Delta.HasValue ? rateStats.Delta.Value.ToString(CultureInfo.InvariantCulture) : "", rateStats.Value.ToString(CultureInfo.InvariantCulture), instrumentId);
                    }
                    else
                    {
                        Log.UpDownCounterRateValuePublished(sessionId, instrument.Meter.Name, instrument.Meter.Version, instrument.Name, instrument.Unit, Helpers.FormatTags(stats.Labels),
                            rateStats.Delta.HasValue ? rateStats.Delta.Value.ToString(CultureInfo.InvariantCulture) : "", rateStats.Value.ToString(CultureInfo.InvariantCulture), instrumentId);
                    }
                }
                else if (stats.AggregationStatistics is LastValueStatistics lastValueStats)
                {
                    Log.GaugeValuePublished(sessionId, instrument.Meter.Name, instrument.Meter.Version, instrument.Name, instrument.Unit, Helpers.FormatTags(stats.Labels),
                        lastValueStats.LastValue.HasValue ? lastValueStats.LastValue.Value.ToString(CultureInfo.InvariantCulture) : "", instrumentId);
                }
                else if (stats.AggregationStatistics is SynchronousLastValueStatistics synchronousLastValueStats)
                {
                    Log.GaugeValuePublished(sessionId, instrument.Meter.Name, instrument.Meter.Version, instrument.Name, instrument.Unit, Helpers.FormatTags(stats.Labels),
                        synchronousLastValueStats.LastValue.ToString(CultureInfo.InvariantCulture), instrumentId);
                }
                else if (stats.AggregationStatistics is HistogramStatistics histogramStats)
                {
                    Log.HistogramValuePublished(sessionId, instrument.Meter.Name, instrument.Meter.Version, instrument.Name, instrument.Unit, Helpers.FormatTags(stats.Labels), FormatQuantiles(histogramStats.Quantiles),
                        histogramStats.Count, histogramStats.Sum, instrumentId);
                }
                else if (stats.AggregationStatistics is Base2ExponentialHistogramStatistics base2ExponentialHistogramStats)
                {
                    Log.Base2ExponentialHistogramValuePublished(
                        sessionId,
                        instrument.Meter.Name,
                        instrument.Meter.Version,
                        instrument.Name,
                        instrumentId,
                        instrument.Unit,
                        Helpers.FormatTags(stats.Labels),
                        base2ExponentialHistogramStats.Scale,
                        base2ExponentialHistogramStats.Sum,
                        base2ExponentialHistogramStats.Count,
                        base2ExponentialHistogramStats.ZeroCount,
                        base2ExponentialHistogramStats.Minimum,
                        base2ExponentialHistogramStats.Maximum,
                        FormatBuckets(base2ExponentialHistogramStats.PositiveBuckets));
                }
            }

            private static string FormatBuckets(long[] buckets)
            {
                ValueStringBuilder sb = new ValueStringBuilder(stackalloc char[512]);

                if (buckets.Length > 0)
                {
                    sb.Append($"{buckets[0]}");
                }

                for (int i = 1; i < buckets.Length; i++)
                {
                    sb.Append($", {buckets[i]}");
                }

                return sb.ToString();
            }

            private static string FormatQuantiles(QuantileValue[] quantiles)
            {
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < quantiles.Length; i++)
                {
#if NET
                    sb.Append(CultureInfo.InvariantCulture, $"{quantiles[i].Quantile}={quantiles[i].Value}");
#else
                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}={1}", quantiles[i].Quantile, quantiles[i].Value);
#endif
                    if (i != quantiles.Length - 1)
                    {
                        sb.Append(';');
                    }
                }
                return sb.ToString();
            }
        }

        private sealed class MetricSpec
        {
            private const char MeterInstrumentSeparator = '\\';
            public string MeterName { get; }
            public string? InstrumentName { get; }

            public MetricSpec(string meterName, string? instrumentName)
            {
                MeterName = meterName;
                InstrumentName = instrumentName;
            }

            public static MetricSpec Parse(string text)
            {
                int slashIdx = text.IndexOf(MeterInstrumentSeparator);
                if (slashIdx < 0)
                {
                    return new MetricSpec(text.Trim(), null);
                }
                else
                {
                    string meterName = text.AsSpan(0, slashIdx).Trim().ToString();
                    string? instrumentName = text.AsSpan(slashIdx + 1).Trim().ToString();
                    return new MetricSpec(meterName, instrumentName);
                }
            }

            public override string ToString() => InstrumentName != null ?
                MeterName + MeterInstrumentSeparator + InstrumentName :
                MeterName;
        }
    }
}
